<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Apple;

use Exception;
use Srv\Conf\ConfModel\AppleAuthConf;
use Srv\Libs\Common\CommFile;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Common\CommNet;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommTime;
use Srv\Libs\ConfModel\NetworkConf;
use Srv\Libs\Frame\Conf;
use Srv\Libs\Frame\ControllerAbstract;
use Srv\Libs\Plugins\Jwt\JWT;
use Srv\Libs\Storage\DataRedis;

final class AppleDeviceCheck
{
    private string $appleBundleId                   = '';
    private string $appId                           = '';
    private string $authIss                         = '';
    private string $authKeyId                       = '';
    private string $authKeyFile                     = '';
    private string $rootCaCerFile                   = '';
    private string $verifyDeviceUrl                 = 'https://api.devicecheck.apple.com';  // 验证设备
    private bool $isSandbox                         = false;
    private ?DataRedis $CcAuto                      = null;             // CcAuto
    private const APPLE_DEVICE_PREFIX               = 'AP_DEVICE_';     // 缓存令牌前缀

    /**
     * @param AppleAuthConf $AppleAuthConf
     * @param bool $isSandbox
     * __construct
     */
    public function __construct(AppleAuthConf $AppleAuthConf, bool $isSandbox)
    {
        $this->appleBundleId        = $AppleAuthConf->getAppleBundleId();
        $this->appId                = $AppleAuthConf->getAppId();
        $this->authIss              = $AppleAuthConf->getIssId();
        $this->authKeyId            = $AppleAuthConf->getKeyId();
        $this->authKeyFile          = $AppleAuthConf->getAuthKeyFile();
        $this->rootCaCerFile        = $AppleAuthConf->getRootCaCerFile();
        $this->isSandbox            = $isSandbox;
        if($this->isSandbox) $this->verifyDeviceUrl = 'https://api.development.devicecheck.apple.com';
        $this->CcAuto               = ControllerAbstract::getCacheAuto();
    }

    /**
     * @return bool
     * isSandboxEnv
     */
    public function isSandboxEnv():bool
    {
        return $this->isSandbox;
    }

    /**
     * @param string $deviceToken
     * @return bool
     * checkDeviceBit
     */
    public function checkDeviceBit(string $deviceToken):bool
    {
        if(strlen($deviceToken) < 16) return false;
        $payload        = [
            'device_token'      => $deviceToken,
            'transaction_id'    => CommString::genRandStrUuId(),
            'timestamp'         => CommTime::getTimeMilli(),
        ];
        $url            = $this->verifyDeviceUrl.'/v1/validate_device_token';
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => $payload, 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo    = CommNet::getUrlInfo($url, $param);
            $httpCode   = intval($urlInfo['http_code']??-1);
            if($httpCode === 200) return true;
            if($httpCode >= 400 && $httpCode <= 599) break;
        }while(--$tryNum >= 0);
        return false;
    }

    /**
     * @param string $deviceToken
     * @param bool $bit0
     * @param bool $bit1
     * @param string $lastUpdateTime
     * @return bool
     * getDeviceBit
     */
    public function getDeviceBit(string $deviceToken, bool &$bit0, bool &$bit1, string &$lastUpdateTime = ''):bool
    {
        if(strlen($deviceToken) < 16) return false;
        $payload        = [
            'device_token'      => $deviceToken,
            'transaction_id'    => CommString::genRandStrUuId(),
            'timestamp'         => CommTime::getTimeMilli(),
        ];
        $url            = $this->verifyDeviceUrl.'/v1/query_two_bits';
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => $payload, 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo    = CommNet::getUrlInfo($url, $param);                    // https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data
            $httpCode   = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) return false;
            $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));    // {"bit0":false, "bit1":false, "last_update_time":"YYYY-MM "}
            if(isset($bodyData['bit0']) || isset($bodyData['bit1']) || isset($bodyData['last_update_time'])) break;
            if($httpCode === 200) break;
        }while(--$tryNum >= 0);
        $bit0           = boolval($bodyData['bit0']??false);
        $bit1           = boolval($bodyData['bit1']??false);
        $lastUpdateTime = strval($bodyData['last_update_time']??'');
        return true;
    }

    /**
     * @param string $deviceToken
     * @param bool $bit0
     * @param bool $bit1
     * @return bool
     * setDeviceBit
     */
    public function setDeviceBit(string $deviceToken, bool $bit0, bool $bit1):bool
    {
        if(strlen($deviceToken) < 16) return false;
        return $this->updateDeviceBit($deviceToken, ['bit0' => $bit0, 'bit1' => $bit1]);
    }

    /**
     * @param string $deviceToken
     * @param bool $bit0
     * @return bool
     * setDeviceBit0
     */
    public function setDeviceBit0(string $deviceToken, bool $bit0):bool
    {
        if(strlen($deviceToken) < 16) return false;
        return $this->updateDeviceBit($deviceToken, ['bit0' => $bit0]);
    }

    /**
     * @param string $deviceToken
     * @param bool $bit1
     * @return bool
     * setDeviceBit1
     */
    public function setDeviceBit1(string $deviceToken, bool $bit1):bool
    {
        if(strlen($deviceToken) < 16) return false;
        return $this->updateDeviceBit($deviceToken, ['bit1' => $bit1]);
    }

    /**
     * @param string $deviceToken
     * @param array $payload
     * @return bool
     * updateDeviceBit
     */
    private function updateDeviceBit(string $deviceToken, array $payload):bool
    {
        if(strlen($deviceToken) < 16 || count($payload) < 1) return false;
        $payload['device_token']    = $deviceToken;
        $payload['transaction_id']  = CommString::genRandStrUuId();
        $payload['timestamp']       = CommTime::getTimeMilli();
        $url            = $this->verifyDeviceUrl.'/v1/update_two_bits';
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'POST_DATA' => $payload, 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo    = CommNet::getUrlInfo($url, $param);
            $httpCode   = intval($urlInfo['http_code']??-1);
            if($httpCode === 200) return true;
            if($httpCode >= 400 && $httpCode <= 599) break;
        }while(--$tryNum >= 0);
        return false;
    }

    /**
     * @return string
     * getJwtToken
     */
    private function getJwtToken():string
    {
        // https://developer.apple.com/help/account/configure-app-capabilities/create-a-devicecheck-private-key
        // 生成 JWT https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
        $privateKey     = '';
        if(!CommFile::getContent($this->authKeyFile, $privateKey)) return '';
        $cacheKey       = self::APPLE_DEVICE_PREFIX.md5($privateKey);
        $cacheStr       = $this->CcAuto->get($cacheKey);
        if(is_string($cacheStr) && strlen($cacheStr) > 32) return $cacheStr;
        $currTime       = CommTime::getTimeStamp();
        $expTime        = 1800;
        $payload        = [
            'iss'   => $this->authIss,
            'iat'   => $currTime - 60,
            'exp'   => $currTime + $expTime,    // 不能超过6个月
        ];
        try{
            $jwtToken   = JWT::encode($payload, $privateKey, 'ES256', $this->authKeyId, []);
            if(strlen($jwtToken) > 32) $this->CcAuto->set($cacheKey, $jwtToken, max($expTime - 60, 1));
        }catch (Exception $Exception){
            $jwtToken   = '';
        }
        return $jwtToken;
    }
}